﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using AutoMapper;
using AzureDiagMon.Common;
using AzureDiagMon.Common.Algorithms.FragmentedRanges;
using AzureDiagMon.Common.Dtos;
using AzureDiagMon.Common.Helpers.Azure;
using AzureDiagMon.Common.Interfaces.Services;
using AzureDiagMon.Common.Services;
using AzureDiagMon.Common.Helpers;
using Ninject;

namespace AzureDiagMon.Services
{
    /// <summary>
    /// Provides Log Syncrhonization service for Azure Diagnostic logs
    /// </summary>
    public class LogSyncService : ReportableBackgroundService<LogSyncServiceState>, INotifyPropertyChanged, ILogSyncService
    {
        public LogSyncService()
        {
            
        }

        #region Imported Services

        [Inject]
        private IDataStoreService DataStoreService { get; set; }

        [Inject]
        private IAzureAccessService AzureAccessService { get; set; }

        #endregion

        #region Synchronization related

        protected readonly AutoResetEvent WakeUpManualResetEvent = new AutoResetEvent(false);

        #endregion

        #region Public properties

        private bool isEnabled;
        public bool IsEnabled
        {
            get { return isEnabled; }
            set
            {
                isEnabled = value;
                LastRefreshTime = null;
                WakeUpManualResetEvent.Set();
                RaisePropertyChanged("IsEnabled");
            }
        }

        private bool isRefreshing;
        public bool IsRefreshing
        {
            get { return isRefreshing; }
            set
            {
                if (isRefreshing == value)
                    return;

                isRefreshing = value;
                RaisePropertyChanged("IsRefreshing");
            }
        }

        private LogSyncServiceConfiguration configuration;
        public LogSyncServiceConfiguration Configuration
        {
            get
            {
                return configuration;
            }
            set
            {
                lock (SyncRoot)
                {
                    configuration = value;
                }
                RaisePropertyChanged("Configuration");
            }
        }

        private DateTime? lastRefreshTime;
        public DateTime? LastRefreshTime
        {
            get { return lastRefreshTime; }
            set
            {
                lastRefreshTime = value;
                RaisePropertyChanged("LastRefreshTime");
            }
        }

        private DateTime? nextRefreshTime;
        public DateTime? NextRefreshTime
        {
            get { return nextRefreshTime; }
            set
            {
                nextRefreshTime = value;
                RaisePropertyChanged("NextRefreshTime");
            }
        }

        private string remainingTimeDisplayText;
        public string RemainingTimeDisplayText
        {
            get { return remainingTimeDisplayText; }
            private set
            {
                remainingTimeDisplayText = value;
                RaisePropertyChanged("RemainingTimeDisplayText");
            }
        }

        #endregion

        #region Private fields

        private bool immediateSyncRequest;
        private LogSyncServiceConfiguration effectiveConfiguration;

        #endregion

        #region Public method

        public void SyncNow()
        {
            immediateSyncRequest = true;
            WakeUpManualResetEvent.Set();
        }

        #endregion

        #region Background Service

        protected override void Worker()
        {
            while (true)
            {
                if (IsEnabled || immediateSyncRequest)
                {
                    var run = false;
                    effectiveConfiguration = Configuration.Clone() as LogSyncServiceConfiguration;

                    lock (SyncRoot)
                    {
                        if (effectiveConfiguration != null)
                        {
                            run = SetNextRefreshTime();
                            immediateSyncRequest = false;
                        }
                    }

                    if (run)
                    {
                        try
                        {
                            DoSync();
                        }
                        catch (Exception ex)
                        {
                            Trace.TraceError("{0}: Failed to do log synchronization: {1}", GetType().Name, ex);
                        }
                        LastRefreshTime = DateTime.Now;
                        SetNextRefreshTime();
                    }
                }
                else
                {
                    IsRefreshing = false;
                }

                WakeUpManualResetEvent.Reset();

                WaitHandle.WaitAny(new EventWaitHandle[] { StopManualResetEvent, WakeUpManualResetEvent }, 1000);

                if (StopManualResetEvent.WaitOne(0))
                    break;
            }
        }

        private bool SetNextRefreshTime()
        {
            var refreshTime = (!LastRefreshTime.HasValue || immediateSyncRequest)
                ? DateTime.Now : LastRefreshTime.Value.Add(effectiveConfiguration.RefreshInterval);

            var run = DateTime.Now >= refreshTime;
            NextRefreshTime = refreshTime;

            if (!run)
            {
                var rounded = refreshTime.AddMilliseconds(refreshTime.Millisecond);
                RemainingTimeDisplayText = rounded.Subtract(DateTime.Now).ToHumanReadable();
            }
            else
            {
                RemainingTimeDisplayText = "ASAP";
            }

            //Debug.WriteLineIf(!run,
            //    string.Format("LogSync: We have {0} seconds to go.",
            //    Math.Round(NextRefreshTime.Value.Subtract(DateTime.Now)
            //                                    .TotalSeconds)));

            return run;
        }

        const int FlushItemCount = 500;

        private void DoSync()
        {
            Debug.WriteLine("DoSync");
            IsRefreshing = true;

            var sa = Mapper.Map<StorageAccountDto, StorageAccountDto>(effectiveConfiguration.StorageAccount);

            //TODO: Remove these lines (get from filters from configuration)
            DateTime startDate;
            DateTime endDate;

            effectiveConfiguration.CalculatedStartAndEndDate(out startDate, out endDate);

            Debug.WriteLine("LogSync: Synchronizing for range: {0} - {1}", startDate, endDate);

            while (true)
            {
                var downloadedSections = DataStoreService.ListWadLogDownloadedSections(sa, startDate, endDate);
                var loadedSections =
                    downloadedSections.Select(Mapper.Map<WadLogDownloadedSectionDto, LoadedRangeSection<long>>)
                                      .OrderBy(f => f.StartIndex)
                                      .ToList();

                var startIndex = startDate.ToWadLogPartitionKey();
                var endIndex = endDate.ToWadLogPartitionKey();

                //TODO: Handle out of bound sections
                var lastSection = loadedSections.LastOrDefault();
                if (lastSection != null && lastSection.EndIndex > endIndex)
                {
                    lastSection.EndIndex = endIndex;
                }

                var fragmentedRange = new FragmentedRange<long>(startIndex,
                                                                endIndex,
                                                                loadedSections,
                                                                new LastFirstResolverStrategy());
                var nextSection = fragmentedRange.GetNextSection();
                if (nextSection == null)
                {
                    //All sections downloaded => stop
                    break;
                }

                Debug.WriteLine(string.Format("Fetching items in Partition Key range: {0} - {1} ({2} - {3})",
                    (long)nextSection.StartIndex,
                    (long)nextSection.EndIndex,
                    ((long)nextSection.StartIndex).ToLocalDateTime(),
                    ((long)nextSection.EndIndex).ToLocalDateTime()));

                Debug.Indent();
                var sw = Stopwatch.StartNew();

                var items = AzureAccessService.FetchWadLogEntries(sa, (long)nextSection.StartIndex, (long)nextSection.EndIndex);
                sw.Stop();

                Debug.WriteLine(string.Format("Query prepared in {0} ms", sw.ElapsedMilliseconds));

                sw = Stopwatch.StartNew();
                items.Flush(FlushItemCount, (flushItems) => DataStoreService.AddLogEntries(sa, flushItems));
                sw.Stop();

                Debug.WriteLine(string.Format("Items flushed in {0} ms", sw.ElapsedMilliseconds));

                Debug.Unindent();

                DataStoreService.AddWadLogDownloadedSection(
                    new WadLogDownloadedSectionDto()
                    {
                        StorageAccountId = sa.Id,
                        DownloadTime = DateTime.Now,
                        StartKey = (long)nextSection.StartIndex,
                        EndKey = (long)nextSection.EndIndex,
                    }
                );

                //TODO: There could be some missing rows if we partly downloaded the items from the last partition. Consolidation removes section endings. (Is it possible?)
                DataStoreService.ConsolidateWadLogDownloadedSections(sa);

            }

            IsRefreshing = false;
            Debug.WriteLine("DoSync done");
        }

        #endregion

        #region Implementation of INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}
